﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using ParserGenerator.Exceptions;
using ParserGenerator.Extensions;
using ParserGenerator.GrammarConstruction;
using ParserGenerator.ScannerConstruction;
using ParserGenerator.TableCompression;

namespace ParserGenerator.ParserConstruction
{
    [Serializable]
    public sealed class ParserAutomatonTable
    {
        public ParserAutomaton Automaton { get; }
        public ReadOnlyCollection<string> GoalNameArray { get; }
        public ReadOnlyCollection<string> NonterminalNameArray { get; }
        public ReadOnlyCollection<string> TokenTextArray { get; }
        public ReadOnlyCollection<int> GoalArray { get; }
        public ReadOnlyCollection<int> LeftArray { get; }
        public ReadOnlyCollection<int> LengthArray { get; }
        public CompactTable ActionTable { get; }
        public ScannerAutomatonTable ScannerTable { get; }

        public ParserAutomatonTable(ParserAutomaton automaton)
        {
            if (automaton == null)
                throw new ArgumentNullException(nameof(automaton));
            Automaton = automaton;
            GoalNameArray = ComputeGoalNameArray();
            NonterminalNameArray = ComputeNonterminalNameArray();
            TokenTextArray = ComputeTokenTextArray();
            GoalArray = ComputeGoalArray();
            LeftArray = ComputeLeftArray();
            LengthArray = ComputeLengthArray();
            ActionTable = ComputeActionTable();
            ScannerTable = new ScannerAutomatonTable(automaton.ScannerAutomaton);
        }

        private ReadOnlyCollection<string> ComputeGoalNameArray()
        {
            string[] goal = new string[Automaton.Goals.Count];
            foreach (var t in Automaton.Goals)
                goal[t.Value.Number] = t.Value.Name;
            return goal.AsReadOnly();
        }

        private ReadOnlyCollection<string> ComputeNonterminalNameArray()
        {
            string[] nonterminal = new string[Automaton.Nonterminals.Count];
            foreach (GrammarNonterminalSymbol sym in Automaton.Nonterminals)
                nonterminal[sym.Number] = sym.Name;
            return nonterminal.AsReadOnly();
        }

        private ReadOnlyCollection<string> ComputeTokenTextArray()
        {
            string[] token = new string[Automaton.Tokens.Count];
            foreach (GrammarTerminalSymbol t in Automaton.Tokens)
                token[t.Number] = t.Text;
            return token.AsReadOnly();
        }

        private ReadOnlyCollection<int> ComputeGoalArray()
        {
            int[] goal = new int[Automaton.Goals.Count];
            foreach (var t in Automaton.Goals)
                goal[t.Value.Number] = t.Value.State.Number;
            return goal.AsReadOnly();
        }

        private ReadOnlyCollection<int> ComputeLeftArray()
        {
            int[] left = new int[Automaton.Productions.Count];
            foreach (GrammarProduction prod in Automaton.Productions)
                left[prod.Number] = prod.Owner.Number;
            return left.AsReadOnly();
        }

        private ReadOnlyCollection<int> ComputeLengthArray()
        {
            int[] length = new int[Automaton.Productions.Count];
            foreach (GrammarProduction prod in Automaton.Productions)
                length[prod.Number] = prod.Length;
            return length.AsReadOnly();
        }

        private Dictionary<GrammarTerminalSymbol, ParserAutomatonState> ComputeShiftActions(ParserAutomatonState state)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            var dict = new Dictionary<GrammarTerminalSymbol, ParserAutomatonState>();
            foreach (ParserAutomatonAction action in state.Table.Actions[state].Values)
                if (action.Shift != null)
                    dict.Add(action.Shift.Symbol, action.Shift.Target);
            return dict;
        }

        private Dictionary<GrammarTerminalSymbol, List<GrammarProduction>> ComputeReduceActions(ParserAutomatonState state)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            var dict = new Dictionary<GrammarTerminalSymbol, List<GrammarProduction>>();
            if (!state.IsConsistent)
                foreach (ParserAutomatonReduction reduce in state.Reductions.Values)
                    foreach (GrammarTerminalSymbol sym in reduce.LookaheadSet)
                        dict.AddValueItem(sym, reduce.Production);
            return dict;
        }

        private void ComputeGotoActions(CompactTableBuilder table)
        {
            if (table == null)
                throw new ArgumentNullException(nameof(table));
            CompactTableRowBuilder[] rows = new CompactTableRowBuilder[Automaton.Nonterminals.Count];
            foreach (GrammarNonterminalSymbol sym in Automaton.Nonterminals)
                rows[sym.Number] = table.DefineRow();
            foreach (ParserAutomatonState state in Automaton.States)
                foreach (ParserAutomatonTransition t in state.Transitions.Values)
                    if (t.IsGoto)
                        rows[t.Symbol.Number].Add(state.Number, t.Target.Number);
        }

        private void UseShiftAction(CompactTableRowBuilder row, GrammarTerminalSymbol token, ParserAutomatonState target)
        {
            if (row == null)
                throw new ArgumentNullException(nameof(row));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            row.Add(token.Number, target.Number << 4 | 0x1);
        }

        private void UseErrorAction(CompactTableRowBuilder row, GrammarTerminalSymbol token)
        {
            if (row == null)
                throw new ArgumentNullException(nameof(row));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            row.Add(token.Number, 0x8);
        }

        private void UseReduceAction(ParserAutomatonState state, CompactTableRowBuilder row, GrammarTerminalSymbol token, List<GrammarProduction> productions, StringBuilder sb)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (row == null)
                throw new ArgumentNullException(nameof(row));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (productions == null)
                throw new ArgumentNullException(nameof(productions));
            if (sb == null)
                throw new ArgumentNullException(nameof(sb));
            if (productions.Count > 1)
            {
                LogReduceReduceConflict(state, token, productions, sb);
                return;
            }
            GrammarProduction prod = productions[0];
            if (prod.IsAccept)
                row.Add(token.Number, prod.Number << 4 | 0x6);
            else
                row.Add(token.Number, prod.Number << 4 | 0x2);
        }

        private void LogReduceReduceConflict(ParserAutomatonState state, GrammarTerminalSymbol token, List<GrammarProduction> productions, StringBuilder sb)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (productions == null)
                throw new ArgumentNullException(nameof(productions));
            if (sb == null)
                throw new ArgumentNullException(nameof(sb));
            if (sb.Length > 0)
                sb.Append("\n\n");
            sb.AppendFormat("{0}\n{1}", state.Number, new string('-', 40));
            sb.AppendFormat("\nreduce-reduce conflict on {0}:", token.Name);
            foreach (GrammarProduction prod in productions)
                sb.AppendFormat("\n  reduce: {0} . {{{1}}}", prod, token.Name);
        }

        private void LogShiftReduceConflict(ParserAutomatonState state, GrammarTerminalSymbol token, ParserAutomatonState target, List<GrammarProduction> productions, StringBuilder sb)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            if (productions == null)
                throw new ArgumentNullException(nameof(productions));
            if (sb == null)
                throw new ArgumentNullException(nameof(sb));
            if (sb.Length > 0)
                sb.Append("\n\n");
            sb.AppendFormat("{0}\n{1}", state.Number, new string('-', 40));
            sb.AppendFormat("\nshift-reduce conflict on {0}:", token.Name);
            ParserAutomatonItemSet closure = state.Core.ComputeClosure(true);
            foreach (ParserAutomatonItem item in closure)
                if (item.IsShift && item.Symbol == token)
                    sb.AppendFormat("\n   shift: {0}", item);
            foreach (GrammarProduction prod in productions)
                sb.AppendFormat("\n  reduce: {0} . {{{1}}}", prod, token.Name);
        }

        private void ResolveShiftReduce(ParserAutomatonState state, CompactTableRowBuilder row, GrammarTerminalSymbol token, ParserAutomatonState target, List<GrammarProduction> productions, StringBuilder sb)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (row == null)
                throw new ArgumentNullException(nameof(row));
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            if (productions == null)
                throw new ArgumentNullException(nameof(productions));
            if (sb == null)
                throw new ArgumentNullException(nameof(sb));
            if (productions.Count > 1)
            {
                LogReduceReduceConflict(state, token, productions, sb);
                return;
            }
            GrammarProduction prod = productions[0];
            if (token.Precedence != null && prod.Precedence != null)
            {
                int shiftPrecedence = token.Precedence.Value;
                int reducePrecedence = prod.Precedence.Value;
                bool chooseError = reducePrecedence == shiftPrecedence &&
                                   token.Associativity.Value == OperatorAssociativity.Nonassociative;
                bool chooseReduce = reducePrecedence > shiftPrecedence ||
                                    reducePrecedence == shiftPrecedence &&
                                    token.Associativity.Value != OperatorAssociativity.Right;
                if (chooseError)
                    UseErrorAction(row, token);
                else if (chooseReduce)
                    UseReduceAction(state, row, token, productions, sb);
                else
                    UseShiftAction(row, token, target);
            }
            else
            {
                if (token.Resolution == null)
                    LogShiftReduceConflict(state, token, target, productions, sb);
                else if (token.Resolution == ShiftReduceResolution.Reduce)
                    UseReduceAction(state, row, token, productions, sb);
                else
                    UseShiftAction(row, token, target);
            }
        }

        private CompactTable ComputeActionTable()
        {
            int tokens = Automaton.Tokens.Count;
            int states = Automaton.States.Count;
            int columns = Math.Max(tokens, states);
            CompactTableBuilder table = new CompactTableBuilder(columns);
            StringBuilder sb = new StringBuilder();
            foreach (ParserAutomatonState state in Automaton.States)
            {
                var shift = ComputeShiftActions(state);
                var reduce = ComputeReduceActions(state);
                CompactTableRowBuilder row = table.DefineRow();
                var usedTokens = new HashSet<GrammarTerminalSymbol>();
                usedTokens.UnionWith(shift.Keys);
                usedTokens.UnionWith(reduce.Keys);
                foreach (GrammarTerminalSymbol token in usedTokens)
                {
                    ParserAutomatonState target;
                    List<GrammarProduction> productions;
                    if (!reduce.TryGetValue(token, out productions))
                    {
                        target = shift[token];
                        UseShiftAction(row, token, target);
                        continue;
                    }
                    if (!shift.TryGetValue(token, out target))
                    {
                        UseReduceAction(state, row, token, productions, sb);
                        continue;
                    }
                    ResolveShiftReduce(state, row, token, target, productions, sb);
                }
            }
            if (sb.Length > 0)
            {
                Console.Error.WriteLine(sb.ToString());
                GrammarErrorException e = new GrammarErrorException(SR.LR1ConflictsDetected);
                throw e;
            }
            ComputeGotoActions(table);
            return table.Build();
        }
    }
}
